package comm;


import java.io.IOException;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;

import java.util.concurrent.LinkedBlockingQueue;

import comm.ClientSideMessage.Type;
/**
 * 
 * Communication interface for a Moodish server. A Moodish server must 
 * communicate clients exclusively through this interface.
 * @author alc
 * @version 1
 */

public class ServerInterface implements ServerComm{
	public static int PORTO = 8082;

	private ServerSocket serverSocket = null;
	private HashMap<String, SocketHandler> socketHandlersMap ;
	private LinkedBlockingQueue<ServerSideMessage> toServerMessageQueue;


	/**
	 * Initializes socketsHandlersMap that will be used to track who connects and disconnects
	 * from the network and toServerMessageQueue that will be used to queue the messages received
	 * from the clients.
	 */
	public ServerInterface(){
		socketHandlersMap = new HashMap<String, SocketHandler>();
		toServerMessageQueue = new LinkedBlockingQueue<ServerSideMessage>();
	}
	/**
	 * Starts the Moodish server. The method should setup a server socket and begin to accept connections.
	 * This method must return immediately after a server socket has been setup, and another thread should
	 * started to listen for connections.
	 */
	@Override
	public void start() {
		try {
			serverSocket = new ServerSocket(PORTO);	

		} catch (IOException e) {
			System.err.println("Could not listen on port: " + PORTO);	
		}
		SocketListener s_listner = new SocketListener(serverSocket, this);
		Thread t = new Thread(s_listner);
		t.start();
	}
	/**
	 * Get the next message received from one of the clients. If no message 
	 * is has been received, the method blocks until a message has 
	 * been received. 
	 * 
	 * @return The next message sent by one of the connected clients to the server
	 */
	@Override
	public ServerSideMessage getNextMessage() {
		return toServerMessageQueue.poll();
	}
	/** 
	 * Checks if a message from a client is pending. If {@link #hasNextMessage()} returns <b>true</b>, a call to {@link #getNextMessage()} 
	 * will return immediately with the oldest message in the queue. If {@link #hasNextMessage()} returns <b>false</b>, a call to {@link #getNextMessage()} 
	 * will block until a message has been received.
	 * 
	 * @return <b>true</b> if a message from a client is currently pending, otherwise <b>false</b>.
	 */
	@Override
	public boolean hasNextMessage() { 
		return !toServerMessageQueue.isEmpty();
	}
	/**
	 * Send a message to a client. This method should be used to relay messages received from a client to a friend. This method should 
	 * be called once for each friend.
	 * 
	 * @param fromNickname The nickname of the client from which the message was sent = sender
	 * @param toNickname   The nickname of a friend									 = receiver	
	 * @param moodishMessage      The message 
	 */
	@Override
	public void sendMoodishMessage(String fromNickename, String toNickname, String moodishMessage) {
		ClientMessage newMoodishMessage = new ClientMessage( fromNickename, moodishMessage, Type.MOODISH_MESSAGE );
		sendMenssageToClient(newMoodishMessage , toNickname);
	}
	/**
	 * Inform a client that another client is now a friend.
	 * @param toNickname  nickname of the client who has a new friend = receiver
	 * @param newFriendship nickname of the new friend                = sender
	 */
	@Override
	public void sendNewFriendship(String toNickname, String newFriendship) { //TODO
		ClientMessage newFriendRequest = new ClientMessage( newFriendship , Type.FRIENDSHIP );
		sendMenssageToClient(newFriendRequest , toNickname);
	}
	/**
	 * Inform a client that a former friend has decided to not be friend anymore.
	 * 
	 * @param toNickname  nickname of the client who has lost a friend = receiver
	 * @param unfriendship nickname of the unfriendship				   = sender	
	 */
	@Override
	public void sendNewUnfriendship(String toNickname, String unfriendship) { //TODO
		ClientMessage removeFriendship = new ClientMessage(  unfriendship, Type.UNFRIENDSHIP );
		sendMenssageToClient(removeFriendship , toNickname);
	}
	/**
	 * Report an error to a client. An error can, for instance, be trying to be friend of  
	 * a client who is not connected, trying to unfriend of a client not currently our  
	 * friend, and so on.
	 * 
	 * @param toNickname nickname of the client.
	 * @param error description of the error.
	 */
	@Override
	public void sendError(String toNickname, String error) { //TODO
		ClientMessage newErrorMessage = new ClientMessage( error, Type.ERROR );
		sendMenssageToClient(newErrorMessage , toNickname);
	}
	/**
	 * Inform a client that a new client has connected. When a new client connects, the server should call this method 
	 * for each client already connected to inform the newly connected client about the clients currently online. 
	 * 
	 *  @param toNickname		nickname of the client to whom to send the message. = receiver
	 *  @param connectedNickname		nickname of the connected client.			= sender
	 */
	@Override
	public void sendClientConnected(String toNickname, String connectedNickname) { //TODO
		ClientMessage newConnectedRequest = new ClientMessage(connectedNickname, Type.CONNECTED);
		sendMenssageToClient(newConnectedRequest, toNickname);
	}

	/**
	 * Inform a client that another client disconnected. 
	 * 
	 * @param toNickname nickname of the client to whom to send the message. = receiver
	 * @param disconnectedNickname nickname of client who disconnected.		 = sender
	 */
	@Override
	public void sendClientDisconnected(String toNickname, String disconnectedNickname) { //TODO

		ClientMessage newDisconnectedRequest = new ClientMessage(disconnectedNickname, Type.DISCONNECTED );
		sendMenssageToClient(newDisconnectedRequest , toNickname);
	}
	/**
	 * Checks if a client with a certain nickname is currently connected.
	 * 
	 * @param nickname nickname of the client to check.
	 * @return <b>true</b> if a client with the nickname is currently connected, otherwise <b>false</b>.
	 */
	@Override
	public boolean clientIsConnected(String nickname) {
		return socketHandlersMap.containsKey(nickname);
	}
	/**
	 * Disconnect a client that is connected.
	 * 
	 * @param nickname 		the nickname of the client that will be disconnected.
	 */
	@Override
	public void disconnectClient(String disconnector) { 
		if(socketHandlersMap.containsKey(disconnector)){
			ClientMessage newDisconnectedRequest = new ClientMessage(disconnector, Type.DISCONNECTED );
			sendMenssageToClient(newDisconnectedRequest , disconnector);
			socketHandlersMap.remove(disconnector);
		}
		else {
			//ERROR TODO
		}
	}




	//este método serve para enviar as mensagens através do socket
	/**
	 *	Sends a message to a client that is currently connected.
	 */
	synchronized void sendMenssageToClient(ClientMessage message , String toNickname){
		if(	socketHandlersMap.containsKey(toNickname)){
			socketHandlersMap.get(toNickname).sendMenssage(message);	
		}	
		System.out.println("Total connected clients: "+socketHandlersMap.size());
	}

	// Método que permite as threads adicionar mensagens à fila de mensagens, este método tem de ser sincronizado!!
	/**
	 * Add message to the queue.
	 */
	synchronized void addMessageToList(ServerMessage message){
		toServerMessageQueue.add(message);
	}
	/**
	 * Add handler to the HashMap 
	 *  
	 * @param nickname	the nickname of the client that connected
	 * @param handler	the handler of the connection
	 */
	synchronized void addHandlerToMap(String nickname, SocketHandler handler){
		socketHandlersMap.put(nickname, handler);
	}


	/**
	 * Thread that listen and will accept the connection when a new client connects.
	 */
	private class SocketListener implements Runnable{

		ServerSocket server_socket;
		private boolean error = false;
		private ServerInterface comm;

		public SocketListener(ServerSocket s , ServerInterface comm){
			this.server_socket = s;
			this.comm = comm;
		}

		@Override
		public void run() {  
			while(!error){
				System.out.println("@SocketListener-SERVER@ Starting to listen for connections...!");
				try {
					System.out.println("@SocketListener-SERVER@......try");
					Socket newConnection = server_socket.accept();		//accept connection
					System.out.println("@SocketListener-SERVER@ New connection accepted!!!");

					SocketHandler socketHandler = new SocketHandler(newConnection, comm);  //binding
					socketHandler.start(); //

				} catch (IOException e) {
					try {
						server_socket.close();
					} catch (IOException e1) {
						System.out.println("@SocketListener-SERVER@"  +" Closing server socket...! inside catch");
					}
					System.out.println("@SocketListener-SERVER@"  +" Closing server socket...! outside catch");
					error = true;
				}

			}
		}

	}
	//------------------------ END of SocketListener -------------------------

	// METODOS CRIADOS PARA FAZER DESPISTE DA APLICACAO

	//	public HashMap<String, SocketHandler> getSocketHandlersMap(){
	//		return this.socketHandlersMap;
	//	}
	//
	/**
	 * Shutdown the server interface
	 */
	public void shutdown(){
		try {
			System.out.println("SERVER SOCKET is being closed...!");
			serverSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	//	/**
	//	 * This is auxiliar method to let change purpose for JUnit tests purpose
	//	 * @param newPort
	//	 */
	/**
	 * Modifies the Port of the Server Interface.
	 * 
	 * @param newPort 		the new Port assigned.
	 */
	public void setPort(int newPort){
		PORTO = newPort;
	}
}
